// !类型守卫
function foo(input: string | number) {
  if (typeof input === 'string') {

  }
  if (typeof input === 'number') { }
  // ...
}

declare const strOrNumOrBool: string | number | boolean;

if (typeof strOrNumOrBool === 'string') {
  // 一定是字符串！
  strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
  // 一定是数字！
  strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
  // 一定是布尔值！
  strOrNumOrBool === true;
} else {
  // 要是走到这里就说明有问题！
  // const _exhaustiveCheck: never = strOrNumOrBool;
  // throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}



function isString1(input: unknown): boolean {
  return typeof input === 'string';
}
function foo1(input: string | number) {
  // 想象类型控制流分析这条河流，刚流进 if (isString(input)) 就戛然而止了。因为 isString 这个函数在另外一个地方，内部的判断逻辑并不在函数 foo 中。这里的类型控制流分析做不到跨函数上下文来进行类型的信息收集（但别的类型语言中可能是支持的）。
  //实际上，将判断逻辑封装起来提取到函数外部进行复用非常常见。为了解决这一类型控制流分析的能力不足， TypeScript 引入了 is 关键字来显式地提供类型信息：
  if (isString1(input)) {
    // (input as string).replace('lys', 'lys123')
    // (input).replace('lys', 'lys123')   // 报错了因为 input 被ts认为是 string | number
  }

  if (typeof input === 'number') { }
}


// 使用is
/*
在isString2的返回值中，我们不再使用 boolean 作为类型标注，而是使用 input is string 这么个奇怪的搭配，拆开来看它是这样的：

input 函数的某个参数；
is string，即 is 关键字 + 预期类型，即如果这个函数成功返回为 true，那么 is 关键字前这个入参的类型，就会被这个类型守卫调用方后续的类型控制流分析收集到。
*/
function isString2(input: unknown): input is string {
  return typeof input === 'string';
}
function foo2(input: string | number) {
  if (isString2(input)) {
    // 正确了
    (input).replace("linbudu", "linbudu599")
  }
}

// $ 需要注意的是，类型守卫函数中并不会对判断逻辑和实际类型的关联进行检查：
function isString3(input: unknown): input is number {
  return typeof input === "string";
}
function foo3(input: string | number) {
  if (isString3(input)) {

    // (input).replace("linbudu", "linbudu599")  // 报错，在这里变成了 number 类型
  }
  if (typeof input === 'number') { }
  // ...
}


//除了使用简单的原始类型以外，我们还可以在类型守卫中使用对象类型、联合类型等，比如下面我开发时常用的两个守卫：
export type Falsy = false | '' | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;

export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean', 'undefined'].includes(typeof val);


// ## 基于 in 与 instanceof 的类型保护
interface Foo {
  foo: string;
  fooOnly: boolean;
  shared: number;
}

interface Bar {
  bar: string;
  barOnly: boolean;
  shared: number;
}

function handle(input: Foo | Bar) {
  if ('foo' in input) {
    input.fooOnly
  } else {
    input.barOnly;
  }
}

//如果用 shared 来区分，就会发现在分支代码块中 input 仍然是初始的联合类型：
/*
function handle2(input: Foo | Bar) {
  if ('shared' in input) {
    //     // 类型“Foo | Bar”上不存在属性“fooOnly”。类型“Bar”上不存在属性“fooOnly”。
    input.fooOnly;
  } else {
    //     // 类型“never”上不存在属性“barOnly”。
    input.barOnly;
  }
}
 */

// 这个可辨识属性可以是结构层面的，比如结构 A 的属性 prop 是数组，而结构 B 的属性 prop 是对象，或者结构 A 中存在属性 prop 而结构 B 中不存在。

// 它甚至可以是共同属性的字面量类型差异：

function ensureArray(input: number | number[]): number[] {
  if (Array.isArray(input)) {
    return input;
  } else {
    return [input]
  }
}

interface Foo {
  kind: 'foo';
  diffType: string;
  fooOnly: boolean;
  shared: number;
}
interface Bar {
  kind: 'bar';
  diffType: number;
  barOnly: boolean;
  shared: number;
}

function handle1(input: Foo | Bar) {
  if (input.kind === 'foo') {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}

//对于同名但不同类型的属性，我们需要使用字面量类型的区分，并不能使用简单的 typeof：
/*
function handle2(input: Foo | Bar) {
  if (typeof input.diffType === 'string') {
    // 报错，并没有起到区分的作用，在两个代码块中都是 Foo | Bar
    input.fooOnly;
  } else {
    input.barOnly;
  }
}
*/

// instanceof 也可以用来进行类型保护：
(function () {
  class FooBase { }

  class BarBase { }

  class Foo extends FooBase {
    fooOnly() { }
  }

  class Bar extends BarBase {
    barOnly() { };
  }

  function handle(input: Foo | Bar) {
    if (input instanceof FooBase) {
      input.fooOnly()
    } else {
      input.barOnly();
    }
  }
}())

